﻿#region License
/* Copyright (C) 2009 Tim Coulter
 * 
 * This file is part of ExtremeML.
 * 
 * ExtremeML is free software: you can redistribute
 * it and/or modify it under the terms of the GNU General Public
 * License, as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace ExtremeML.Spreadsheet.Address
{
	/// <summary>
	/// Parses and decomposes a native address reference string to extract other cell addressing types.
	/// </summary>
	internal static class AddressParser
	{
		private const string MatchAbsolute = @"\$?";
		private const string MatchA1Column = @"[a-df-wyzA-Z]{1,3}";
		private const string MatchA1Row = @"\d{1,6}";
		private const string MatchA1Cell = MatchAbsolute + MatchA1Column + MatchAbsolute + MatchA1Row;
		private const string MatchRowSpan = @"\(" + MatchAbsolute + MatchA1Row + ":" + MatchAbsolute + MatchA1Row + @"\)";
		private const string MatchColumnSpan = @"\(" + MatchAbsolute + MatchA1Column + ":" + MatchAbsolute + MatchA1Column + @"\)";
		private const string MatchA1Area = MatchA1Cell + ":" + MatchA1Cell;
		private const string MatchAddresses = "(" + MatchA1Area + "|" + MatchRowSpan + "|" + MatchColumnSpan + "|" + MatchA1Cell + ")";

		/// <summary>
		/// Parses an Excel function expression and returns the set of objects representing its
		/// components. These objects are either instances of the various addressing classes or strings.
		/// </summary>
		/// <param name="expression">An Excel function expression to parse.</param>
		/// <returns>The decomposed elements of the passed function expression.</returns>
		public static IEnumerable<object> ParseExpression(
			string expression)
		{
			var elements = Regex.Split(expression, MatchAddresses);

			if (elements.Length == 1)
			{
				yield return elements[0];
			}
			else
			{
				for (var i = 0; i < elements.Length; i++)
				{
					if (i % 2 == 0)
					{
						// Even-numbered elements are static sub-strings
						yield return elements[i];
					}
					else
					{
						// Odd-numbered elements are address references
						yield return ParseAddress(elements[i]);
					}
				}
			}
		}

		/// <summary>
		/// Parses an Excel address reference and creates an address object of the appropriate type.
		/// </summary>
		/// <param name="reference">An address string that represents a cell, area, row span or column span.</param>
		/// <returns>An <see cref="IAddressDependency"/> representing a strongly-typed address object.</returns>
		internal static IAddressDependency ParseAddress(
			string reference)
		{
			if (Regex.Match(reference, MatchA1Area).Value == reference)
			{
				return ParseCellArea(reference);
			}
			else if (Regex.Match(reference, MatchRowSpan).Value == reference)
			{
				return ParseRowSpan(reference);
			}
			else if (Regex.Match(reference, MatchColumnSpan).Value == reference)
			{
				return ParseColumnSpan(reference);
			}
			else if (Regex.Match(reference, MatchA1Cell).Value == reference)
			{
				return ParseCellAddress(reference);
			}
			else
			{
				throw new ArgumentException("The passed value does not match a recognised address format.");
			}
		}

		/// <summary>
		/// Parses the passed cell area reference and creates a strongly-typed address object.
		/// </summary>
		/// <param name="reference">An address reference in the format <c>$A1:$A1</c> or <c>$A1</c>.</param>
		/// <returns>A new <see cref="CellArea"/> instance.</returns>
		/// <remarks>
		/// The passed address reference string may use absolute or relative addressing and may
		/// explictly define both the beginning and end cells in the range or may omit the end
		/// cell reference, in which case it will be implicitly treated as a single cell range.
		/// </remarks>
		internal static CellArea ParseCellArea(
			string reference)
		{
			if (string.IsNullOrEmpty(reference)) return new CellArea();

			var pattern =
				MakeRegexGroup("begin", MatchA1Cell) +
				"(?<optional>:" +
				MakeRegexGroup("end", MatchA1Cell) +
				")?";

			var match = Regex.Match(reference, pattern);

			if (match.Success && match.Groups["begin"].Captures.Count == 1)
			{
				var begin = ParseCellAddress(match.Groups["begin"].Value);

				if (match.Groups["end"].Captures.Count == 1)
				{
					// Explicit end of range
					return new CellArea(begin, ParseCellAddress(match.Groups["end"].Value));
				}
				else
				{
					// Implicit end of range
					return new CellArea(begin, ParseCellAddress(match.Groups["begin"].Value));
				}
			}
			else
			{
				throw new ArgumentException("The passed value does not match the designated address format.");
			}
		}

		/// <summary>
		/// Parses the passed row span reference and creates a strongly-typed address object.
		/// </summary>
		/// <param name="reference">An address reference in the format <c>$1:$1</c> or <c>$1</c>.</param>
		/// <returns>A new <see cref="RowSpan"/> instance.</returns>
		/// <remarks>
		/// The passed address reference string may use absolute or relative addressing and may
		/// explictly define both the beginning and end rows in the range or may omit the end
		/// row reference, in which case it will be implicitly treated as a single row span.
		/// </remarks>
		internal static RowSpan ParseRowSpan(
			string reference)
		{
			if (string.IsNullOrEmpty(reference)) return new RowSpan();

			var pattern =
				MakeRegexGroup("abs_begin", MatchAbsolute) +
				MakeRegexGroup("begin", MatchA1Row) +
				"(?<optional>:" +
				MakeRegexGroup("abs_end", MatchAbsolute) +
				MakeRegexGroup("end", MatchA1Row) +
				")?";

			var match = Regex.Match(reference, pattern);

			if (match.Success && match.Groups["begin"].Captures.Count == 1)
			{
				var begin = int.Parse(match.Groups["begin"].Value) - 1;
				var absBegin = match.Groups["abs_begin"].Value == "$";

				if (match.Groups["end"].Captures.Count == 1)
				{
					// Explicit end of range
					return new RowSpan(
						begin,
						int.Parse(match.Groups["end"].Value) - 1,
						absBegin,
						match.Groups["abs_end"].Value == "$");
				}
				else
				{
					// Implicit end of range
					return new RowSpan(begin, begin, absBegin, absBegin);
				}
			}
			else
			{
				throw new ArgumentException("The passed value does not match the designated address format.");
			}
		}

		/// <summary>
		/// Parses the passed column span reference and creates a strongly-typed address object.
		/// </summary>
		/// <param name="reference">An address reference in the format <c>$A:$A</c> or <c>$A</c>.</param>
		/// <returns>A new <see cref="ColumnSpan"/> instance.</returns>
		/// <remarks>
		/// The passed address reference string may use absolute or relative addressing and may
		/// explictly define both the beginning and end columns in the range or may omit the end
		/// column reference, in which case it will be implicitly treated as a single column span.
		/// </remarks>
		internal static ColumnSpan ParseColumnSpan(
			string reference)
		{
			if (string.IsNullOrEmpty(reference)) return new ColumnSpan();

			var pattern =
				MakeRegexGroup("abs_begin", MatchAbsolute) +
				MakeRegexGroup("begin", MatchA1Column) +
				"(?<optional>:" +
				MakeRegexGroup("abs_end", MatchAbsolute) +
				MakeRegexGroup("end", MatchA1Column) +
				")?";
			
			var match = Regex.Match(reference, pattern);

			if (match.Success && match.Groups["begin"].Captures.Count == 1)
			{
				var begin = DecodeColumnName(match.Groups["begin"].Value);
				var absBegin = match.Groups["abs_begin"].Value == "$";

				if (match.Groups["end"].Captures.Count == 1)
				{
					// Explicit end of range
					return new ColumnSpan(
						begin,
						DecodeColumnName(match.Groups["end"].Value),
						absBegin,
						match.Groups["abs_end"].Value == "$");
				}
				else
				{
					// Implicit end of range
					return new ColumnSpan(begin, begin, absBegin, absBegin);
				}
			}
			else
			{
				throw new ArgumentException("The passed value does not match the designated address format.");
			}
		}

		/// <summary>
		/// Parses the passed cell address reference and creates a strongly-typed address object.
		/// </summary>
		/// <param name="reference">An address reference in the format <c>$A1</c>.</param>
		/// <returns>A new <see cref="CellAddress"/> instance.</returns>
		internal static CellAddress ParseCellAddress(
			string reference)
		{
			if (string.IsNullOrEmpty(reference)) return new CellAddress();

			var pattern =
				MakeRegexGroup("abs_column", MatchAbsolute) +
				MakeRegexGroup("column", MatchA1Column) +
				MakeRegexGroup("abs_row", MatchAbsolute) +
				MakeRegexGroup("row", MatchA1Row);

			var match = Regex.Match(reference, pattern);

			if (match.Success && match.Groups["column"].Captures.Count == 1 && match.Groups["row"].Captures.Count == 1)
			{
				return new CellAddress(new GridReference(
					int.Parse(match.Groups["row"].Value) - 1,
					DecodeColumnName(match.Groups["column"].Value)),
					match.Groups["abs_row"].Value == "$",
					match.Groups["abs_column"].Value == "$");
			}
			else
			{
				throw new ArgumentException("The passed value does not match the designated address format.");
			}
		}

		/// <summary>
		/// Creates a Regex matching group expression using the passed name and sub-expression.
		/// </summary>
		private static string MakeRegexGroup(
			string name,
			string expression)
		{
			return string.Format("(?<{0}>{1})", name, expression);
		}

		/// <summary>
		/// Converts a native Excel alphabetic column name into a zero-based integer column index.
		/// </summary>
		/// <param name="name">A <see cref="string"/> that specifies the alphabetic column name.</param>
		public static int DecodeColumnName(
			string name)
		{
			int result = 0;

			for (var i = 0; i < name.Length; i++)
			{
				result += (int)((Convert.ToInt32(name[name.Length - i - 1]) - 64) * Math.Pow(26, i));
			}

			return result - 1;
		}

		/// <summary>
		/// Converts a zero-based column integer index into a native Excel alphabetic column name.
		/// </summary>
		/// <param name="columnIndex">An <see cref="int"/> that specifies the zero-based column index.</param>
		/// <returns>A <see cref="string"/> that represents the alphabetic name the specified column.</returns>
		public static string EncodeColumnName(
			int columnIndex)
		{
			if (columnIndex < 0) throw new ArgumentOutOfRangeException("columnIndex");
	
			var dividend = columnIndex + 1;
			var result = String.Empty;

			while (dividend > 0)
			{
				var modulo = (dividend - 1) % 26;
				result = Convert.ToChar(65 + modulo) + result;
				dividend = (dividend - modulo) / 26;
			}

			return result;
		}
	}
}